import * as argon2 from "argon2";
import AppError from "@/common/error";

export type PasswordEncoder = {
  encode: (password: string) => Promise<string>;
  verify: (rawPassword: string, hash: string) => Promise<boolean>;
};

const NOOP_ENCODER: PasswordEncoder = {
  async encode(password) {
    return password;
  },
  async verify(rawPassword, hash) {
    return rawPassword === hash;
  },
};

const ARGON2_ENCODER: PasswordEncoder = {
  async encode(password) {
    return await argon2.hash(password);
  },
  async verify(rawPassword, hash) {
    return await argon2.verify(hash, rawPassword);
  },
};

class DelegatePasswordEncoder implements PasswordEncoder {
  constructor(
    private defaultId: string,
    private readonly delegates: Record<string, PasswordEncoder>,
  ) {
    if (!this.delegates[this.defaultId]) {
      throw AppError.internalServerError500(`No password encoder found for id ${this.defaultId}`);
    }
    if (Object.keys(this.delegates).length === 0) {
      throw AppError.internalServerError500("No password encoder found.");
    }
  }

  private getEncoder(id: string): PasswordEncoder {
    const encoder = this.delegates[id];
    if (!encoder) {
      throw AppError.internalServerError500(`No password encoder found for id ${id}`);
    }
    return encoder;
  }

  async encode(password: string): Promise<string> {
    const id = this.defaultId;
    const encoder = this.getEncoder(id);
    const encoded = await encoder.encode(password);
    return `${id}::${encoded}`;
  }

  private splitHash(hash: string): [string, string] {
    const segments = hash.split("::");
    if (segments.length !== 2) {
      throw AppError.internalServerError500("Invalid password hash");
    }
    return segments as [string, string];
  }

  async verify(rawPassword: string, hash: string): Promise<boolean> {
    const [id, encoded] = this.splitHash(hash);
    const encoder = this.getEncoder(id);
    return encoder.verify(rawPassword, encoded);
  }
}

const DEFAULT: PasswordEncoder = new DelegatePasswordEncoder("argon2", {
  argon2: ARGON2_ENCODER,
  noop: NOOP_ENCODER,
});

export default DEFAULT;
